import processing.core.*; 
import processing.data.*; 
import processing.event.*; 
import processing.opengl.*; 

import controlP5.*; 
import java.io.File; 
import java.nio.ByteBuffer; 
import javax.sound.midi.MidiEvent; 
import javax.sound.midi.MidiMessage; 
import javax.sound.midi.MidiSystem; 
import javax.sound.midi.MetaMessage; 
import javax.sound.midi.Sequence; 
import javax.sound.midi.ShortMessage; 
import javax.sound.midi.Track; 
import javax.sound.midi.*; 
import java.util.Arrays; 
import java.util.Random; 
import arb.soundcipher.*; 

import java.util.HashMap; 
import java.util.ArrayList; 
import java.io.File; 
import java.io.BufferedReader; 
import java.io.PrintWriter; 
import java.io.InputStream; 
import java.io.OutputStream; 
import java.io.IOException; 

public class FractalPlayer extends PApplet {

//
// MIDIAnalyser is a soft for MID extension song statistical analysis
// Calculating fractal dimmension and detecting fractal sign of a musical piece 
// Generating melody based on a loaded composition
// Created as part of Vilnius University bachelor work: Fractals application in musical sound analysis.
// Author: Aleksandr Voronovic
//

MidSong midSong;
StatCalculator statCalculator;
FractalFinder finder;
Plotter plotDrawer;
SCScore score;
GUI ui;
boolean debug;
int generateLen;
int leaveLen;
String currentTrackName;

TrackStats testTrack;
boolean doSaveFile;

public void setup() {
  size(800, 640, P2D);
  debug = false;
  doSaveFile = true;
  generateLen = 10;
  currentTrackName = "";
  midSong = new MidSong();
  plotDrawer = new Plotter();
  ui = new GUI();
}


public void calculateStats(int i){
  statCalculator = new StatCalculator(midSong);
  statCalculator.claculateSongStatistics();

  score = new SCScore();
  score.tempo(midSong.tempo);
  score.instrument(statCalculator.tracks.get(i).instrument);

  finder = new FractalFinder(statCalculator.tracks.get(i));
  setupSliders();
  plotDrawer.notesList = finder.calcNotesPoints();
  plotDrawer.durationsList = finder.calcDurationsPoints();
  plotDrawer.velocityesList = finder.calcVelocityesPoints();
}


public void draw() {
  background(20);
  text(currentTrackName, width/2-currentTrackName.length()*4, 30);
  plotDrawer.plot();
}

class FractalFinder {
  TrackStats stats;
  float sclaeFactor;
  float measureSize;


  FractalFinder(TrackStats stats) {
    this.stats = stats;
  }

  public ArrayList<PVector> calcNotesPoints() {
    ArrayList<PVector> list = new ArrayList<PVector>(); 
    for (int i = 0; i < stats.notes.size(); i++) {
      list.add(new PVector(i*10, stats.notes.get(i)*5));
    }
    return list;
  }

  public ArrayList<PVector> calcVelocityesPoints() {
    ArrayList<PVector> list = new ArrayList<PVector>(); 
    for (int i = 0; i < stats.velocityes.size(); i++) {
      list.add(new PVector(i*10, stats.velocityes.get(i)*5));
    }
    return list;
  }

  public ArrayList<PVector> calcDurationsPoints() {
    ArrayList<PVector> list = new ArrayList<PVector>(); 
    for (int i = 0; i < stats.durations.size(); i++) {
      list.add(new PVector(i*10, getDurationNum(stats.durations.get(i))*5));
    }
    return list;
  }

  public ArrayList<PVector> calcPointNotes(ArrayList<PVector> points, int position) {
    ArrayList<PVector> list = new ArrayList<PVector>(); 
    for (int i = position; i < points.size(); i++) {
      PVector point = points.get(i);
      list.add(new PVector(point.x/10, point.y/5));
    }
    return list;
  }

  public ArrayList<PVector> measureCurve(ArrayList<PVector> points) {
    ArrayList<PVector> lengths = new ArrayList<PVector>();
    for (float rulerSize = 5; rulerSize < 10; rulerSize++) {
      float perimeter = 0;
      for (int i = 0; i < points.size()-1; i++) {
        float distance = dist(points.get(i).x, points.get(i).y, points.get(i+1).x, points.get(i+1).y);
        float step = (float)ceil(distance/rulerSize);
        if (step < 0) step = 1.0f;
        perimeter += step;
      }
      lengths.add(new PVector(perimeter, 1/rulerSize));
    }
    return lengths;
  }


  public ArrayList<PVector> calcLogParams(ArrayList<PVector> params) {
    ArrayList<PVector> logStats = new ArrayList<PVector>();
    for (int i = 0; i < params.size(); i++) {
      float logMeasure =(float) Math.log10(params.get(i).x);
      float logScale =(float) Math.log10(params.get(i).y);

      logStats.add(new PVector(logMeasure, logScale));
    }

    return logStats;
  }

  public float calcSlope(ArrayList<PVector> params) {
    double a = params.get(0).x-params.get(params.size()-1).x;
    double b = params.get(0).y-params.get(params.size()-1).y;

    return Float.valueOf(String.format("%.3f", 1+(a/b)));
  }


  public ArrayList<PVector> nextNoteRandom(ArrayList<PVector> params, float aimDimension) {
    boolean noteFits = false;
    ArrayList<PVector> newNotesList = new ArrayList<PVector>();
    long seed = rnd.nextLong();
    setupFbm(stats.getNotesSD(), params.size());
    fbm(2-aimDimension, seed);
    int i = 0;
    float avg = avg(stats.notes);
    while (!noteFits) {
      newNotesList = params;
      float possibleNote = constrain(avg + round(X[i]), stats.notesRange.x, stats.notesRange.y);
      newNotesList.add(new PVector(newNotesList.size()*10, possibleNote*5));

      float slope = calcSlope(calcLogParams(measureCurve(newNotesList)));
      if (slope == aimDimension) {
        //println("note");
        noteFits = true;
      }
      else {
        newNotesList.remove(newNotesList.size()-1);
      }
      if (i < X.length-1) {
        i++;
      }
      else {
        //    println("new seed for notes");
        i = 0;
        seed = rnd.nextLong();
        fbm(2-aimDimension, seed);
      }
    }   
    return newNotesList;
  }

  public ArrayList<PVector> nextDurationRandom(ArrayList<PVector> params, float aimDimension) {
    boolean noteFits = false;
    ArrayList<PVector> newNotesList = new ArrayList<PVector>();
    long seed = rnd.nextLong();
    setupFbm(stats.getDurationsSD(), params.size());

    fbm(2-aimDimension, seed);
    int i = 0;
    float avg = avgDur(stats.durations);
    while (!noteFits) {
      newNotesList = params;
      float possibleNote = constrain(avg + round(X[i]*42), stats.durationRange.x, stats.durationRange.y);//round(map(X[i], -0.024, 0.054, 1, 7)); //constrain(round(X[i]), stats.durationRange.x, stats.durationRange.y);
      newNotesList.add(new PVector(newNotesList.size()*10, possibleNote*5));

      float slope = calcSlope(calcLogParams(measureCurve(newNotesList)));
      if (slope == aimDimension || true) {
        noteFits = true;
      }
      else {
        newNotesList.remove(newNotesList.size()-1);
      }
      if (i < X.length-1) {
        i++;
      }
      else {
        i = 0;
        seed = rnd.nextLong();
        fbm(2-aimDimension, seed);
      }
    }   
    return newNotesList;
  }

  public ArrayList<PVector> nextVelocityRandom(ArrayList<PVector> params, float aimDimension) {
    boolean noteFits = false;
    ArrayList<PVector> newNotesList = new ArrayList<PVector>();
    long seed = rnd.nextLong();
    setupFbm(stats.getVelocityesSD(), params.size());
    fbm(2-aimDimension, seed);
    int i = 0;
    float avg = avg(stats.velocityes);
    while (!noteFits) {
      newNotesList = params;
      float possibleNote =  constrain(avg + round(X[i]), stats.velocityRange.x, stats.velocityRange.y); // constrain(round(X[i]) + (stats.velocityRange.x + stats.velocityRange.y)/2, 0, 127);
      newNotesList.add(new PVector(newNotesList.size()*10, possibleNote*5));

      float slope = calcSlope(calcLogParams(measureCurve(newNotesList)));

      if (slope == aimDimension) {
        //println("velocity");
        noteFits = true;
      }
      else {
        newNotesList.remove(newNotesList.size()-1);
      }
      if (i < X.length-1) {
        i++;
      }
      else {
        i = 0;
        seed = rnd.nextLong();
        fbm(2-aimDimension, seed);
      }
    }   
    return newNotesList;
  }
}

public float uberRandom(PVector constrains) {
  float a = floor(random(constrains.x, constrains.y));
  float b = floor(random(constrains.x, constrains.y));
  float c = floor(random(constrains.x, constrains.y));

  float val = a+b+c/3;
  if (val > 128.0f) {
    val = 128.0f;
  }
  return floor(val);
}

public float fNoise(PVector constains, float time) {

  float n = noise(time) * constains.y;
  //println(round(n));
  return round(n);
}

public float getDurationNum(float val) {
  if (val == 1/64.0f) return 1.0f;
  if (val == 1/32.0f) return 2.0f;
  if (val == 1/16.0f) return 3.0f;
  if (val == 1/8.0f) return 4.0f;
  if (val == 1/4.0f) return 5.0f;
  if (val == 1/2.0f) return 6.0f;
  if (val == 1.0f) return 7.0f;
  return 0;
}

public float getDurationVal(float val) {
  if (val == 1.0f) return  1/64.0f;
  if (val == 2.0f) return 1/32.0f;
  if (val == 3.0f) return 1/16.0f;
  if (val == 4.0f) return 1/8.0f;
  if (val == 5.0f) return 1/4.0f;
  if (val == 6.0f) return 1/2.0f;
  if (val == 7.0f) return 1.0f;
  return 0;
}




String[] filenames;
int[] tracks;
ListBox songsList;
ListBox trackList;
Slider generateCount;
Slider leaveOriginalCount;
Button runGeneration;
CheckBox checkbox;

class GUI {
  ControlP5 cp5;

  Button openFileButton;

  GUI() {
    cp5 = new ControlP5(FractalPlayer.this);

    openFileButton = cp5.addButton("OpenFile")
      .setPosition(30, 30)
        .setSize(70, 19);

    runGeneration = cp5.addButton("run")
      .setPosition(660, 60)
        .setSize(70, 19)
          .setVisible(false);

    songsList = cp5.addListBox("Songs")
      .setPosition(30, 80)
        .setSize(170, 160)
          .setItemHeight(20)
            .setBarHeight(20);

    songsList.captionLabel().style().marginTop = 5;
    songsList.valueLabel().style().marginTop = 3;

    trackList = cp5.addListBox("Tracks")
      .setPosition(230, 80)
        .setSize(160, 220)
          .setItemHeight(20)
            .setBarHeight(20);

    trackList.captionLabel().style().marginTop = 5;
    trackList.valueLabel().style().marginTop = 3;   

    addAvalableSongs();
  }
}

public void setupSliders() {
  ui.cp5.remove(ui.cp5.getController("generate"));
  ui.cp5.remove(ui.cp5.getController("leave"));

  // add a vertical slider
  generateCount = ui.cp5.addSlider("generate")
    .setPosition(430, 60)
      .setSize(200, 20)
        .setRange(0, 200)
          .setValue(10);

  // reposition the Label for controller 'slider'
  generateCount.getValueLabel().align(ControlP5.LEFT, ControlP5.BOTTOM_OUTSIDE).setPaddingX(0);
  generateCount.getCaptionLabel().align(ControlP5.RIGHT, ControlP5.BOTTOM_OUTSIDE).setPaddingX(0);

  // add a vertical slider
  leaveOriginalCount = ui.cp5.addSlider("leave")
    .setPosition(430, 100)
      .setSize(200, 20)
        .setRange(0, finder.stats.notes.size())
          .setValue(0);

  // reposition the Label for controller 'slider'
  leaveOriginalCount.getValueLabel().align(ControlP5.LEFT, ControlP5.BOTTOM_OUTSIDE).setPaddingX(0);
  leaveOriginalCount.getCaptionLabel().align(ControlP5.RIGHT, ControlP5.BOTTOM_OUTSIDE).setPaddingX(0);
}

public void OpenFile(int theValue) {
  selectInput("Select a MID file to process:", "fileSelected");
}

public void generate(int number) {
  generateLen = number;
}

public void leave(int number) {
  leaveLen = number;
}

public void run(int theValue){
  score.stop();
  score.update();
  generateNotes(generateLen, leaveLen); 
}


public void fileSelected(File selection) {
  try {
    midSong.loadSongFP(selection.getAbsolutePath());
    println();
  }
  catch(Exception e) {
    println("File selection canceled");
  }
}

public void controlEvent(ControlEvent theEvent) {
  if (theEvent.isGroup()) {
    // an event from a group e.g. scrollList
    println(theEvent.group().value()+" from "+theEvent.group());
  }

  if (theEvent.isGroup() && theEvent.name().equals("Songs")) {
    int test = (int)theEvent.group().value();
    midSong.loadSong(filenames[test]);
    currentTrackName = filenames[test];
  }

  if (theEvent.isGroup() && theEvent.name().equals("Tracks")) {
    int test = (int)theEvent.group().value();
    runGeneration.setVisible(true);
    calculateStats(test);
  }
}

public void addAvalableSongs() {
  java.io.File folder = new java.io.File(dataPath("")+"/songs/");

  java.io.FilenameFilter midFilter = new java.io.FilenameFilter() {
    public boolean accept(File dir, String name) {
      return name.toLowerCase().endsWith(".mid");
    }
  };

  filenames = folder.list(midFilter);

  for (int i = 0; i < filenames.length; i++) {
    ListBoxItem lbi = songsList.addItem(filenames[i], i);
  }
}

// MIDINote holds the note name, duration and other parameters

class MIDINote {
  String noteName;
  int key;
  int velocity;
  float duration;
  long startTick;
  long endTick;
  boolean closed;

  MIDINote(String noteName, int key, int velocity, long startTick) {
    this.noteName = noteName;
    this.key = key;
    this.velocity = velocity;
    this.startTick = startTick;
    this.closed = false;
  }

  public void setDuration(int value) {
    this.duration = value;
  }

  MIDINote(MIDINote another) {
    this.noteName = another.noteName;
    this.key = another.key;
    this.velocity =  another.velocity;
    this.duration = another.duration;
    this.startTick = another.startTick;
    this.endTick = another.endTick;
    this.closed = another.closed;
  }
}

class MIDITrack {
  ArrayList<MIDINote> pendingNotes;
  ArrayList<MIDINote> closedNotes;
  ArrayList<Integer> pendingRemoval;
  PVector timeSignature;
  String name;
  Instrument instrument;
  int instrumentNumber;
  String instrumentName;

  MIDITrack() {
    pendingNotes = new ArrayList<MIDINote>();
    closedNotes = new ArrayList<MIDINote>();
    pendingRemoval = new ArrayList<Integer>();
  }

  public void addNote(String noteName, int key, int velocity, long tick) {
    MIDINote note = new MIDINote(noteName, key, velocity, tick);
    if (debug) println("# adding note: " + key + " at "+ tick);
    long lastTick = getLatestTick();
    if (lastTick < note.startTick) {
      fillRests(lastTick, note.startTick);
    }
    pendingNotes.add(note);
  }

  public void addClosedNote(float duration, MIDINote note) {
    note.closed = true;
    note.duration = duration;
    note.endTick = note.startTick + round(duration*4*timeResolution);
    if (debug) println("# adding closed note: " + note.key + " at "+ note.startTick);
    closedNotes.add(note);
  }

  public void addRest(float duration, long startTick) {
    if (debug) println("adding rest of dur: " + duration);
    MIDINote rest = new MIDINote("r", 128, 0, startTick);
    rest.closed = true;
    rest.duration = duration;
    rest.endTick = startTick + round(duration*4*timeResolution);
    closedNotes.add(rest);
  }

  public void closeNote(String noteName, int key, int velocity, long tick) {
    for (int i = 0; i < pendingNotes.size(); i++) {
      if (pendingNotes.get(i).key == key && !pendingNotes.get(i).closed) {
        if (debug) println("# 1 closing note: " + key + " at "+ tick );
        MIDINote newNote = pendingNotes.get(i);
        pendingNotes.get(i).closed = true;
        long delta = tick - newNote.startTick;
        fillNotes(newNote, delta);
      }
    }
//    for (int n : pendingRemoval) {
//      pendingNotes.remove(n);
//    }
//    pendingRemoval.clear();
  }

  public long getLatestTick() {
    long tick = 0;
    for (int i = 0; i < closedNotes.size(); i++) {
      if (closedNotes.get(i).endTick > tick) {
        tick = closedNotes.get(i).endTick;
      }
    }
    return tick;
  }

  public void fillRests(long from, long to) {
    long count = to - from;
    long startTick = from;

    long fullNotes = count/(timeResolution*4);
    count = count%(timeResolution*4); 

    long halfNotes = count/(timeResolution*2);
    count = count%(timeResolution*2);       

    long forthNotes = count/timeResolution;
    count = count%timeResolution;

    long eithNotes = count/(timeResolution/2);
    count = count%(timeResolution/2);

    long sixteenthNotes = count/(timeResolution/4);
    count = count%(timeResolution/4);

    long thNotes = count/(timeResolution/8);
    count = count%(timeResolution/8);

    long sfNotes = count/(timeResolution/16);

    for (long a = 0; a < fullNotes; a++) {
      addRest(1, startTick);
      startTick += timeResolution*4;
    }
    for (long b = 0; b < halfNotes; b++) {
      addRest(0.5f, startTick);
      startTick += timeResolution*2;
    }
    for (long c = 0; c < forthNotes; c++) {
      addRest(0.25f, startTick);
      startTick += timeResolution;
    }
    for (long d = 0; d < eithNotes; d++) {
      addRest(0.125f, startTick);
      startTick += timeResolution/2;
    }
    for (long e = 0; e < sixteenthNotes; e++) {
      addRest(0.0625f, startTick);
      startTick += timeResolution/4;
    }
    for (long f = 0; f < thNotes; f++) {
      addRest(0.03125f, startTick);
      startTick += timeResolution/8;
    }
    for (long g = 0; g < sfNotes; g++) {
      addRest(0.015625f, startTick);
      startTick += timeResolution/16;
    }
  }

  public void fillNotes(MIDINote note, long ticks) {
    //println(" # fill Notes " + note.key+ " " + ticks);
    long count = ticks;
    long startTick = note.startTick;
    
    long fullNotes = count/(timeResolution*4);
    count = count%(timeResolution*4);
    
    long halfNotes  = count/(timeResolution*2);
    count = count%(timeResolution*2);   
    
    long forthNotes = count/timeResolution;
    count = count%timeResolution;
    
    long eithNotes = count/(timeResolution/2);
    count = count%(timeResolution/2);
    
    long sixteenthNotes = count/(timeResolution/4);
    count = count%(timeResolution/4);
    
    long thNotes = count/(timeResolution/8);
    count = count%(timeResolution/8);
    
    long sfNotes = count/(timeResolution/16);

    for (long a = 0; a < fullNotes; a++) {
      addClosedNote(1.0f, note);
      startTick += timeResolution*4;
    }
    for (long b = 0; b < halfNotes; b++) {
      addClosedNote(0.5f, note);
      startTick += timeResolution*2;
    }
    for (long c = 0; c < forthNotes; c++) {
      addClosedNote(0.25f, note);
      startTick += timeResolution;
    }
    for (long d = 0; d < eithNotes; d++) {
      addClosedNote(0.125f, note);
      startTick += timeResolution/2;
    }
    for (long e = 0; e < sixteenthNotes; e++) {
      addClosedNote(0.0625f, note);
      startTick += timeResolution/4;
    }
    for (long f = 0; f < thNotes; f++) {
      addClosedNote(0.03125f, note);
      startTick += timeResolution/8;
    }
    for (long g = 0; g < sfNotes; g++) {
      addClosedNote(0.015625f, note);
      startTick += timeResolution/16;
    }
  }
}

// MIDISOng class holds all information about a song
// And all the tracks of different instruments












int timeResolution;

class MidSong {
  final  int NOTE_ON = 0x90;
  final  int NOTE_OFF = 0x80;
  final  String[] NOTE_NAMES = {
    "C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B"
  };
  ArrayList<MIDITrack> tracks;
  PVector timeSignature;
  Sequence sequence;
  int trackNumber;
  Instrument[] orchestra;
  int tempo;
  int pulse;

  MidSong() {
    tracks = new ArrayList<MIDITrack>();
    loadInstruments();
    trackNumber = 0;
    timeSignature = new PVector(4, 4);
  }

  public void loadSong(String name) {
    removeSong();
    sequence  = null;
    try {
      sequence = MidiSystem.getSequence(new File(dataPath("")+"/songs/"+name));
      timeResolution = sequence.getResolution();
      readSong();
    }
    catch(Exception e) {
      println(e);
    }
  }

  public void loadSongFP(String name) {
    removeSong();
    sequence  = null;
    try {
      sequence = MidiSystem.getSequence(new File(name));
      currentTrackName = name.substring(name.lastIndexOf('/')+1, name.length());
      timeResolution = sequence.getResolution();
      readSong();
    }
    catch(Exception e) {
      println(e);
    }
  }

  public void removeSong() {
    tracks.clear();
    trackNumber = 0;
    timeSignature = new PVector(4, 4);
  }

  public void readSong() {
    println("Reading MID song...");
    for (Track track :  sequence.getTracks()) {
      tracks.add(new MIDITrack());
      if (timeSignature != null) {
        tracks.get(trackNumber).timeSignature = timeSignature;
      }
      println("Track " + trackNumber + ": size = " + track.size());
      //println();
      for (int i=0; i < track.size(); i++) {
        MidiEvent event = track.get(i);
        if (debug) {
          print("@" + event.getTick() + " ");
        }
        MidiMessage message = event.getMessage();
        processMessage(message, event.getTick());
      }
      println();
    }
    removeEmptyTracks();
    trackList.clear();
    for (int i = 0; i < tracks.size(); i++) {
      //println(tracks.get(i).instrument.getName());
      //println(tracks.get(i).name);
      String name = tracks.get(i).name;
      if (name == null || name.equals("")){
        name = tracks.get(i).instrument.getName();
      }
      ListBoxItem lbi = trackList.addItem(name, i);
    }
    println("Song read! recorded "+ tracks.size()+" tacks.");
  }

  public void processMessage(MidiMessage message, long tick) {
    if (message instanceof MetaMessage) {
      processMetaMessage(message);
    }
    else if (message instanceof ShortMessage) {
      processShortMessage(message, tick);
    }
    else {
      if (debug) {
        println("@ Other message: " + message.getClass());
      }
    }
  }

  public void processMetaMessage(MidiMessage message) {
    MetaMessage mm = (MetaMessage) message;
    String tmp;
    switch(mm.getType()) {
    case 1:
      tmp = new String(mm.getData());
      println("Sequence number: " + tmp);
      break;     
    case 2:
      tmp = new String(mm.getData());
      println("Text: " + tmp);
      break;        
    case 3:
      tmp = new String(mm.getData());
      println("Track name: " + tmp);
      tracks.get(trackNumber).name = tmp;
      break;
    case 4:
      tmp = new String(mm.getData());
      println("Instrument: " + tmp);
      tracks.get(trackNumber).instrumentName = tmp;
      break;
    case 6:
      tmp = new String(mm.getData());
      println("Marker: " + tmp);
      tracks.get(trackNumber).instrumentName = tmp;
      break;      

    case 81:
      byte[] buf = mm.getData();
      int r = (buf[2] & 0xFF) | ((buf[1] & 0xFF) << 8) | ((buf[0] & 0x0F) << 16);
      int bpm = 60000000 / r; 
      tempo = bpm;
      break;
    case 88:
      byte[] buf2 = mm.getData();
      println("@ Time signature: " + buf2[0] +"/"+ buf2[1]*buf2[1]);
      println("@ Metronome pulse : " + buf2[2]);
      println("@ 32th notes per MIDI-quarter note : " + buf2[3]);
      tracks.get(trackNumber).timeSignature = new PVector(buf2[0], buf2[1]*buf2[1]);
      timeSignature = new PVector(buf2[0], buf2[1]*buf2[1]);
      pulse = buf2[3];
      break;
    case 89:
      byte[] buf3 = mm.getData();
      println("@ Key signature: " + buf3[0] +" sharps and "+ buf3[1]+" flats");

      break;
    case 47:
      println("@ End of track");
      trackNumber++;
      break;
    default:
      print("@ Other Meta message: " + mm.getClass());
      println(" Type: " + mm.getType());
      break;
    }
  }

  public void processShortMessage(MidiMessage message, long tick) {
    ShortMessage sm = (ShortMessage) message;
    //print("Channel: " + sm.getChannel() + " ");
    if (sm.getCommand() == NOTE_ON) {
      int key = sm.getData1();
      int octave = (key / 12)-1;
      int note = key % 12;
      String noteName = NOTE_NAMES[note];
      int velocity = sm.getData2();
      if (velocity > 0) {
        if (debug) {
          println("Note on, " + noteName + octave + " key=" + key + " velocity: " + velocity);
          println();
        }
        tracks.get(trackNumber).addNote(noteName+octave, key, velocity, tick);
      }
      else {
        if (debug) {

          println("Note off, " + noteName + octave + " key=" + key + " velocity: " + velocity);
          println();
        }
        tracks.get(trackNumber).closeNote(noteName+octave, key, velocity, tick);
      }
    } 
    else if (sm.getCommand() == NOTE_OFF) {
      int key = sm.getData1();
      int octave = (key / 12)-1;
      int note = key % 12;
      String noteName = NOTE_NAMES[note];
      int velocity = sm.getData2();
      if (debug) {
        println("Note off, " + noteName + octave + " key=" + key + " velocity: " + velocity);
        println();
      }
      tracks.get(trackNumber).closeNote(noteName+octave, key, velocity, tick);
    } 
    else if (sm.getCommand() >= 192 && sm.getCommand() <= 207) {
      int ch = sm.getCommand() - 192 + 1;

      println("@ "+ orchestra[sm.getData1()] + " to channel "+ ch);
      tracks.get(trackNumber).instrumentNumber = sm.getData1(); 
      tracks.get(trackNumber).instrument = orchestra[sm.getData1()];
    }
    else {
      if (debug) {
        println("Other Command:" + sm.getCommand());
      }
    }
  }


  public void removeEmptyTracks() {
    for (int i = 0; i < tracks.size(); i++) {
      if (tracks.get(i).closedNotes.size() == 0) {
        tracks.remove(i);
        removeEmptyTracks();
      }
    }
  }

  public void loadInstruments() {
    Synthesizer synthesizer = null;
    try {
      synthesizer = MidiSystem.getSynthesizer();
      synthesizer.open();
    }             
    catch(Exception e) {
      println("Unable to create sythesizer object");
    }
    orchestra = synthesizer.getAvailableInstruments();
  }
}

class Plotter {
  ArrayList<PVector> notesList;
  ArrayList<PVector> durationsList;
  ArrayList<PVector> velocityesList;

  Plotter() {
    notesList = new ArrayList<PVector>();
    durationsList = new ArrayList<PVector>();
    velocityesList = new ArrayList<PVector>();
  }

  public void plot() {
    //background(255);
    stroke(0);
    text("Notes graph", 20, 290);
    drawNotesGraph(20, 300);
    text("Durations graph", 20, 420);
    drawDurationsGraph(20, 430);
    text("Velocities graph", 20, 500);
    drawVelocitiesGraph(20, 510);
  }

  public void drawNotesGraph(int x, int y) {
    int w = width-40;
    int h = 100;
    stroke(40);
    fill(150, 150, 160);
    rect(x, y, w, h);
    pushMatrix();
    translate(x, y);

    //notesList
    for (int i = 0; i < notesList.size(); i++) {
      stroke(20, 80, 125);
      float val = notesList.get(i).x/10;
      float val2 = notesList.get(i).y/5;

      val = map(val, 0, notesList.size(), 5, w-5);
      val2 = map(val2, 0, 128, 5, h-5);
      point(val, val2);

      if (i < notesList.size()-2) {
        float val3 = notesList.get(i+1).x/10;
        float val4 = notesList.get(i+1).y/5;

        val3 = map(val3, 0, notesList.size(), 5, w-5);
        val4 = map(val4, 0, 128, 5, h-5);
        line(val3, val4, val, val2);
      }
    }
    popMatrix();
  }

  public void drawDurationsGraph(int x, int y) {
    int w = width-40;
    int h = 50;
    stroke(40);
    fill(150, 150, 160);
    rect(x, y, w, h);
    pushMatrix();
    translate(x, y);

    //notesList
    for (int i = 0; i < durationsList.size(); i++) {
      stroke(40, 100, 125);
      float val = durationsList.get(i).x/10;
      float val2 = durationsList.get(i).y/5;

      val = map(val, 0, durationsList.size(), 5, w-5);
      val2 = map(val2, 1, 7, 5, h-5);
      point(val, val2);

      if (i < durationsList.size()-2) {
        float val3 = durationsList.get(i+1).x/10;
        float val4 = durationsList.get(i+1).y/5;

        val3 = map(val3, 0, durationsList.size(), 5, w-5);
        val4 = map(val4, 1, 7, 5, h-5);
        line(val3, val4, val, val2);
      }
    }
    popMatrix();
  }

  public void drawVelocitiesGraph(int x, int y) {
    int w = width-40;
    int h = 100;
    stroke(40);
    fill(150, 150, 160);
    rect(x, y, w, h);
    pushMatrix();
    translate(x, y);

    //notesList
    for (int i = 0; i < velocityesList.size(); i++) {
      stroke(20, 40, 125);
      float val = velocityesList.get(i).x/10;
      float val2 = velocityesList.get(i).y/5;

      val = map(val, 0, velocityesList.size(), 5, w-5);
      val2 = map(val2, 0, 127, 5, h-5);
      point(val, val2);

      if (i < notesList.size()-2) {
        float val3 = velocityesList.get(i+1).x/10;
        float val4 = velocityesList.get(i+1).y/5;

        val3 = map(val3, 0, velocityesList.size(), 5, w-5);
        val4 = map(val4, 0, 127, 5, h-5);
        line(val3, val4, val, val2);
      }
    }
    popMatrix();
  }
}

// StatCalculator is a class that analyzes a MidSong object and counts notes and intervals used in the song, 
// theyr frequence and distribution through the song. Reads basic song data e.g. how many instruments are used 


class StatCalculator {
  MidSong song;
  ArrayList<TrackStats> tracks;

  StatCalculator(MidSong song) {
    this.song = song;
    tracks = new ArrayList<TrackStats>();
  }
  
  public void claculateSongStatistics() {
    for (int i = 0; i < song.tracks.size(); i++) {
      MIDITrack currentTrack = song.tracks.get(i);
      tracks.add(new TrackStats(currentTrack.instrumentNumber));
      for (int j = 0; j < currentTrack.closedNotes.size(); j++) {
        MIDINote note = currentTrack.closedNotes.get(j);
        tracks.get(lastTrack()).addParameter(note.key, tracks.get(lastTrack()).notes);
        tracks.get(lastTrack()).addParameter(note.velocity, tracks.get(lastTrack()).velocityes);
        tracks.get(lastTrack()).addParameter(note.duration, tracks.get(lastTrack()).durations);
      }
    }
    println("Calculator read " + tracks.size());
  }

  public int lastTrack() {
    return tracks.size()-1;
  }
}


class TrackStats {
  int instrument;
  PVector notesRange;
  PVector velocityRange;
  PVector durationRange;

  ArrayList<Float> notes;  
  ArrayList<Float> velocityes;
  ArrayList<Float> durations;  

  TrackStats(int instrument) {
    this.instrument = instrument;
    notesRange = new PVector(0, 128);
    velocityRange = new PVector(0, 127);
    durationRange = new PVector(1, 8);
    notes = new ArrayList<Float>();
    velocityes = new ArrayList<Float>();
    durations = new ArrayList<Float>();
  }

  public float getNotesSD() {
    float summ = 0;
    for (float val:notes) {
      summ += val;
    }
    float avg = summ/notes.size();
    float summ2 = 0;
    for (float val:notes) {
      summ2 += pow((val-avg), 2);
    }
    return sqrt(summ2/notes.size());
  }

  public float getDurationsSD() {
    float summ = 0;
    for (float val:durations) {
      summ += val;
    }
    float avg = summ/durations.size();
    float summ2 = 0;
    for (float val:durations) {
      summ2 += pow((val-avg), 2);
    }
    return sqrt(summ2/durations.size());
  }

  public float getVelocityesSD() {
    float summ = 0;
    for (float val:velocityes) {
      summ += val;
    }
    float avg = summ/velocityes.size();
    float summ2 = 0;
    for (float val:velocityes) {
      summ2 += pow((val-avg), 2);
    }
    return sqrt(summ2/velocityes.size());
  }

  public void addParameter(float val, ArrayList<Float> parameters) {
    parameters.add(val);
  }
}

public float avg(ArrayList<Float> params){
  float summ = 0;
  for (int i = 0; i < params.size(); i++){
    summ += params.get(i);
  }
  return round(summ/params.size());
}

public float avgDur(ArrayList<Float> params){
  float summ = 0;
  for (int i = 0; i < params.size(); i++){
    summ += getDurationNum(params.get(i));
  }
  return round(summ/params.size());
}

// Fractal Brownian Motion Modulator



Random rnd = new Random();
int maxLevel = 10;
float[] X = new float[round(pow(2, maxLevel))+1];

float[] delta = new float[maxLevel+1];

float sigma; // standart deviation

public void fbm(float H, long seed) {
  rnd.setSeed(seed);
  for (int i = 1; i <= maxLevel; i++) {
    delta[i] = sigma * pow(0.5f, i*H)*sqrt(0.5f)*sqrt(1-pow(2, 2*H-2));
  }
  int N = round(pow(2, maxLevel));
  X[0] = 0;
  X[N] = sigma * (float) rnd.nextGaussian();
  int D = N;
  int d = D/2;
  int level = 1;
  while (level<=maxLevel) {
    //println(level);
    for (int i = d; i <= N-d; i+=D) {
      X[i] = 0.5f * (X[i-d]+X[i+d]);
    }
    for (int i = 0; i <= N; i+=d) {
      X[i] = X[i] + delta[level]* (float)rnd.nextGaussian();
    }
    D = D/2;
    d = d/2;
    level++;
  }
}

public void setupFbm(float sigma_, int size){
    sigma = sigma_;
    Double tmp = (Math.log(size-1)/Math.log(2));
    maxLevel = tmp.intValue();
    X = new float[round(pow(2, maxLevel))+1];
    delta = new float[maxLevel+1];
}



public void generateNotes(int count, int leave) {
  println("# Calculating plot points...");
  ArrayList<PVector> notesCurve = finder.calcNotesPoints();
  ArrayList<PVector> durationsCurve = finder.calcDurationsPoints();
  ArrayList<PVector> velocityesCurve = finder.calcVelocityesPoints();

  int size = notesCurve.size();

  println("# Getting tracks fractal dimmension...");
  float aimNotesDim = finder.calcSlope(finder.calcLogParams(finder.measureCurve(notesCurve)));
  float aimDuratiobsDim = finder.calcSlope(finder.calcLogParams(finder.measureCurve(durationsCurve)));
  float aimVelocityesDim = finder.calcSlope(finder.calcLogParams(finder.measureCurve(velocityesCurve)));
  println("notes dim: " + aimNotesDim);
  println("duration dim: " + aimDuratiobsDim);
  println("velocity dim: " + aimVelocityesDim);  
  ArrayList<PVector> tempNotesCurve = notesCurve;
  ArrayList<PVector> tempDurationsCurve = durationsCurve;
  ArrayList<PVector> tempVelocityesCurve = velocityesCurve;
  
  println("# Generating new notes...");
  for (int i = 0; i < count; i++) {
    tempNotesCurve = finder.nextNoteRandom(tempNotesCurve, aimNotesDim);
    tempDurationsCurve = finder.nextDurationRandom(tempDurationsCurve, aimDuratiobsDim);
    tempVelocityesCurve = finder.nextVelocityRandom(tempVelocityesCurve, aimVelocityesDim);
  }
  println("# Converting notes to MIDI...");
  //println("leave len: " + leaveLen);
  size -= leaveLen;
  ArrayList<PVector> newNotes = finder.calcPointNotes(tempNotesCurve, size);
  ArrayList<PVector> newDurations = finder.calcPointNotes(tempDurationsCurve, size);
  ArrayList<PVector> newVelocityes = finder.calcPointNotes(tempVelocityesCurve, size);

  playNotes(newNotes, newDurations, newVelocityes);
}

public void playNotes(ArrayList<PVector> notes, ArrayList<PVector> durations, ArrayList<PVector> velocityes) {
  println("# Scoring notes... ");
  score.empty();
  float nextDelta = 0;
  for (int i = 0; i < notes.size(); i++) {

    float velocity = velocityes.get(i).y;
    if (notes.get(i).y == 128.0f) {
      velocity = 0;
    }
    //println("note: " + notes.get(i).y + " duration: " + getDurationVal(durations.get(i).y) + " velocity: " + velocity);
    score.addNote(nextDelta, notes.get(i).y, velocity, getDurationVal(durations.get(i).y));
    nextDelta += getDurationVal(durations.get(i).y)*4;
  }
  // empty note because of cut off
  score.addNote(nextDelta+1, 0, 0, 1);
  println("# Play notes");
  score.play();
  if (doSaveFile) {
    score.writeMidiFile(dataPath("")+"/generated_"+currentTrackName);
  }
}

  static public void main(String[] passedArgs) {
    String[] appletArgs = new String[] { "FractalPlayer" };
    if (passedArgs != null) {
      PApplet.main(concat(appletArgs, passedArgs));
    } else {
      PApplet.main(appletArgs);
    }
  }
}
